#include <string.h>
#include <syslog.h>
#include <stdlib.h>
#include <stdio.h>

#include <iostream>
using namespace std;


#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_PASSWORD

#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/_pam_macros.h>

#include "pam_srp.hpp"

/* argument parsing */
#define PAM_DEBUG_ARG		0x0001
#define PAM_USE_FPASS_ARG	0x0002
#define PAM_TRY_FPASS_ARG	0x0004


namespace
{
    const int MAX_MSG_LEN = 4096;

    const char PW_IS_EXPIRED[] = "SRP_PW_IS_EXPIRED";
}


namespace pam_srp
{
    
    SRPException::SRPException()
    {}


    void SRPException::set_va_message( const char * fmt, va_list args )
    {
        char * buff = new char[ MAX_MSG_LEN ];

        vsnprintf(buff, MAX_MSG_LEN, fmt, args);

        buff[ MAX_MSG_LEN - 1 ] = '\0';
        
        message = buff;

        delete [] buff;
    }

    SRPException::SRPException( const char *fmt, ... )
    {
        va_list args;

        va_start(args, fmt);

        set_va_message(fmt, args);

        va_end(args);
    }

    AuthenticationFailed::AuthenticationFailed( const char *fmt, ... ) 
    {
        va_list args;

        va_start(args, fmt);

        set_va_message(fmt, args);

        va_end(args);
    }

    SRPError::SRPError( const char *fmt, ... )
    {
        va_list args;

        va_start(args, fmt);

        set_va_message(fmt, args);

        va_end(args);

        return_code = PAM_SERVICE_ERR;
        syslog_code = LOG_ERR;
    }
    
    SRPError::SRPError( const std::string & msg ) : SRPException( msg.c_str() )
    {
       return_code = PAM_SERVICE_ERR;
       syslog_code = LOG_ERR;
    }



    int parse_pam_args (pam_handle_t *pamh, int argc, const char **argv,
                        const char **cfg_file, const char **srp_config)
    {
        int ctrl;
       
        *cfg_file    = NULL;
        *srp_config  = NULL;
       
        /* step through arguments */
        for (ctrl = 0; argc-- > 0; ++argv)
        {
            /* generic options */
           
            if (!strcmp(*argv,"debug"))
                ctrl |= PAM_DEBUG_ARG;
            else if (!strcasecmp(*argv, "use_first_pass"))
                ctrl |= PAM_USE_FPASS_ARG;
            else if (!strcasecmp(*argv, "try_first_pass"))
                ctrl |= PAM_TRY_FPASS_ARG;
            else if (!strncasecmp(*argv,"srp_config=", 11))
            {
                *srp_config = (*argv) + 11;
                if (**srp_config == '\0')
                {
                    *srp_config = NULL;
                    throw SRPError("srp_config= missing argument - ignored");
                }
            }
            else if ( *cfg_file == NULL && **argv == '/' )
            {
                *cfg_file = *argv;
            }
            else
            {
                throw SRPError("invalid argument: %s", *argv);
            }
        }

        if ( *cfg_file == NULL )
           *cfg_file = "/etc/srp_auth.conf";
       
        return ctrl;
    }
    

    void srp_authenticate( pam_handle_t *pamh, int flags, int argc, const char **argv )
    {
        SRPSettings settings;
        
        const char *username            = 0;
        const char *password            = 0;
        const char *cfg_file            = 0;
        const char *srp_config          = 0;
        const char *net_error           = 0;
        bool        is_password_expired = true;
        int         retval              = PAM_AUTH_ERR;
        int         ctrl                = 0;

        ctrl = parse_pam_args(pamh, argc, argv, &cfg_file, &srp_config);

        if (!cfg_file)
            throw SRPError("pam_srp module missing required configuration file argument");
        
        settings.parse_file(cfg_file);

        pam_set_item(pamh, PAM_AUTHTOK_TYPE, "SRP");

        // Get the username
        retval = pam_get_user(pamh, &username, 0);
        
        if ((retval != PAM_SUCCESS) || (!username))
            throw SRPError("failed to obtain the username");

        retval = pam_get_authtok(pamh, PAM_AUTHTOK, &password, 0);
        
        if ( retval != PAM_SUCCESS || !password )
        {
           SRPError e("failed to recover user password");
           e.return_code = PAM_AUTHTOK_RECOVERY_ERR;
           throw e;
        }
        
        
        if (ctrl & PAM_DEBUG_ARG)
        {
            pam_syslog(pamh, LOG_AUTH | LOG_INFO, "Verify user `%s' with server `%s' and port %d",
                       username, settings.server.c_str(), settings.port);
        }
                
        try
        {
            authenticate_from_network( settings, username, password,
                                       is_password_expired );

            pam_syslog(pamh, LOG_AUTH | LOG_NOTICE, "user '%s' granted access by %s:%d", username,
                       settings.server.c_str(), settings.port);

            pam_set_data(pamh, PW_IS_EXPIRED, (void *)( is_password_expired ? PW_IS_EXPIRED : 0 ), 0);
            
            if (!is_password_expired)
            {
               const char * cache_error = 0;
               
               cache_credentials( settings, username, password, &cache_error );
               
               if (cache_error)
                  pam_syslog(pamh, LOG_AUTH | LOG_NOTICE, "cache error: %s", cache_error);
            }
        }
        catch( NetworkError & e )
        {
            if (ctrl & PAM_DEBUG_ARG)
                pam_syslog(pamh, LOG_AUTH | LOG_INFO, "communication with %s:%d failed: %s",
                           settings.server.c_str(), settings.port, e.what());
            
            authenticate_from_cache( settings, username, (const char *)password );

            pam_syslog(pamh, LOG_AUTH | LOG_NOTICE, "user '%s' granted access by cached credentials", username);
        }        
    }


    void srp_chauthtok( pam_handle_t *pamh, int flags, int argc, const char **argv )
    {
        SRPSettings settings;

        const char *req_username           = 0;
        const char *tgt_username           = 0;
        const char *req_password           = 0;
        const char *new_password           = 0;
        const char *cfg_file               = 0;
        const char *srp_config             = 0;
        const char *net_error              = 0;
        bool        is_password_expired    = true;
        int         retval                 = PAM_AUTH_ERR;
        int         ctrl                   = 0;
        bool        update_only_if_expired = flags & PAM_CHANGE_EXPIRED_AUTHTOK;


        ctrl = parse_pam_args(pamh, argc, argv, &cfg_file, &srp_config);

        if (!cfg_file)
            throw SRPError("pam_srp module missing required configuration file argument");

        settings.parse_file(cfg_file);

        if ( flags & PAM_PRELIM_CHECK )
        {
           if ( !test_connection(settings) )
           {
              SRPError e("Unable to contact network server");
              e.return_code = PAM_TRY_AGAIN;
              throw e;
           }
           
           return;
        }

        if ( ! (flags & PAM_UPDATE_AUTHTOK) )
           return; // should never hit this

        // Get the username
        retval = pam_get_user(pamh, &tgt_username, 0);
        
        if ((retval != PAM_SUCCESS) || (!tgt_username))
            throw SRPError("failed to obtain the username");

        // The passwd utility cannot support any user other than
        // root changing another user's password. As root will
        // typically not be stored in an SRP database, we'll defer
        // implementing this feature here until a convincing argument
        // is heard.
        req_username = tgt_username;

        pam_set_item(pamh, PAM_AUTHTOK_TYPE, "SRP");

        retval = pam_get_authtok(pamh, PAM_OLDAUTHTOK, &req_password, 0);
        
        if ( retval != PAM_SUCCESS || !req_password )
        {
           SRPError e("failed to obtain user password");
           e.return_code = PAM_AUTHTOK_RECOVERY_ERR;
           throw e;
        }

        retval = pam_get_authtok(pamh, PAM_AUTHTOK, &new_password, 0);
        
        if ( retval != PAM_SUCCESS || !new_password )
        {
           SRPError e("failed to obtain new user password");
           e.return_code = PAM_AUTHTOK_ERR;
           throw e;
        }
        
        
        if (ctrl & PAM_DEBUG_ARG)
        {
            pam_syslog(pamh, LOG_AUTH | LOG_INFO, "User %s is updating password for user `%s' with server `%s' and port %d",
                       req_username, tgt_username, settings.server.c_str(), settings.port);
        }
        
        authenticate_from_network( settings,
                                   req_username, req_password,
                                   is_password_expired,
                                   tgt_username, new_password,
                                   update_only_if_expired);
    }


    
}// end namespace pam_srp




extern "C"
{

    PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
                                       int argc, const char **argv)
    {
        try
        {            
            pam_srp::srp_authenticate(pamh, flags, argc, argv);

            return PAM_SUCCESS;
        }
        catch( pam_srp::AuthenticationFailed & e )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_INFO, "%s", e.what());
            return PAM_AUTH_ERR;
        }
        catch( pam_srp::SRPError & e )
        {
            pam_syslog(pamh, LOG_AUTH | e.syslog_code, "%s", e.what());
            return e.return_code;
        }
        catch( std::bad_alloc & e )
        {
           pam_syslog(pamh, LOG_AUTH | LOG_CRIT, "Memory allocation failed");
        }
        catch( std::exception & e )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_ERR, "%s", e.what());
        }
        catch( ... )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_ERR, "Caught unexpected exception");
        }
        
        return PAM_SERVICE_ERR;
    }

    
    PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh , int flags ,
                                  int argc , const char **argv )
    {
        pam_syslog(pamh, LOG_AUTH | LOG_ERR , "pam_srp does not suppport the setcred function");
        return PAM_SERVICE_ERR;
    }

   
    PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags ,
                                    int argc, const char **argv)
    {
       void * ptr    = 0;
       int    retval = 0;
       
       retval = pam_get_data(pamh, PW_IS_EXPIRED, (const void**)&ptr);

       if (retval != PAM_SUCCESS)
       {          
          retval = pam_sm_authenticate(pamh, flags, argc, argv);

          if (retval == PAM_SUCCESS)
             retval = pam_get_data(pamh, PW_IS_EXPIRED, (const void**)&ptr);
       }

       if (retval == PAM_SUCCESS)
       {
          return ptr ? PAM_NEW_AUTHTOK_REQD : PAM_SUCCESS;             
       }
       else
       {
          return retval;
       }
    }

    PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh , int flags ,
                                       int argc , const char **argv )
    {
        pam_syslog(pamh, LOG_AUTH | LOG_ERR , "pam_srp does not suppport the session function");
        return PAM_SERVICE_ERR;
    }

    PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh , int flags ,
                                        int argc , const char **argv )
    {
        pam_syslog(pamh, LOG_AUTH | LOG_ERR , "pam_srp does not suppport the session function");
        return PAM_SERVICE_ERR;
    }
   
    
    PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags ,
                                    int argc, const char **argv)
    {
       try
        {            
            pam_srp::srp_chauthtok(pamh, flags, argc, argv);

            return PAM_SUCCESS;
        }
        catch( pam_srp::AuthenticationFailed & e )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_INFO, "%s", e.what());
            return PAM_AUTH_ERR;
        }
        catch( pam_srp::SRPError & e )
        {
            pam_syslog(pamh, LOG_AUTH | e.syslog_code, "%s", e.what());
            return e.return_code;
        }
        catch( std::bad_alloc & e )
        {
           pam_syslog(pamh, LOG_AUTH | LOG_CRIT, "Memory allocation failed");
        }
        catch( std::exception & e )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_ERR, "%s", e.what());
        }
        catch( ... )
        {
            pam_syslog(pamh, LOG_AUTH | LOG_ERR, "Caught unexpected exception");
        }
        
        return PAM_SERVICE_ERR;
    }
    
    
#ifdef PAM_STATIC
    
    /* static module data */
    
    struct pam_module _pam_srp_modstruct = {
        "pam_srp",
        pam_sm_authenticate,
        pam_sm_setcred,
        pam_sm_acct_mgmt,
        pam_sm_open_session,
        pam_sm_close_session,
        pam_sm_chauthtok,
    };
#endif

}// end extern "C"


